[Home] Python으로 돌아가기

손글씨 숫자 분류

📖 목차

(1) 문제 정의

딥러닝을 활용하여 손글씨 숫자 이미지(MNIST 데이터셋)를 분류하는 모델을 구축한다. MNIST는 0부터 9까지의 손글씨 숫자 이미지(28x28 픽셀)로 구성된 대표적인 이미지 분류 데이터셋이다. 이 문제는 10개의 클래스를 분류하는 다중 클래스 분류 문제이다.

...
Visualization of the MNIST Test Dataset in the Deep Lake UI
출처: https://datasets.activeloop.ai/docs/ml/datasets/mnist/

(2) 머신러닝 파이프라인


1) 데이터 로딩

TensorFlow의 keras.datasets 모듈을 통해 MNIST 데이터를 로딩한다. 학습 데이터 60,000개와 테스트 데이터 10,000개로 구성되어 있으며, 각 데이터는 28x28 크기의 흑백 이미지이다. 이미지를 시각화하여 각 숫자의 라벨이 잘 매칭되는지 확인한다.

...
데이터와 라벨 샘플
출처: https://cleanlab.ai/blog/label-errors-image-datasets/
...
픽셀로 구성된 MNIST 숫자 데이터
각 이미지는 28x28 = 784 의 구조로 이루어져 있으며, 흑백 이미지이므로 한 개의 채널을 갖는다.
출처: https://hatchworks.com/blog/gen-ai/train-and-fine-tune-multimodal-model/
... ...
MNIST 이미지 데이터 세트
55000개의 Training set의 구조를 본다면, [55000, 784]의 tensor로 구성된다.
이들의 label의 구조는 [55000, 10] (0 ~ 9 의 숫자이므로) 으로 이루어진다.
출처: https://wikidocs.net/136667

2) 데이터 전처리

이미지 데이터를 정규화하기 위해 0 ~ 255의 픽셀 값을 0 ~ 1 사이로 스케일링한다. 또한 모델 입력 형식에 맞게 데이터를 reshape 하며, 라벨은 원-핫 인코딩 처리한다. CNN 모델에서는 이미지 차원을 (28, 28, 1)로 변경한다.

...
스케일링된 MNIST 이미지
출처: https://nesusws-tutorials-bd-dl.readthedocs.io/en/latest/hands-on/tensorflow/mnist/
...
MNIST 레이블 데이터 세트
출처: https://wikidocs.net/136667

3) 모델 구성 및 학습

딥러닝 모델로는 두 가지 방식을 사용한다.

CNN 모델은 Conv2D, MaxPooling2D, Flatten, Dense 계층으로 구성하고, Adam 옵티마이저와 categorical_crossentropy 손실 함수를 사용하여 학습한다.

...
모델 구성
출처: https://wikidocs.net/165440

4) 예측 및 결과

테스트 데이터 중 하나를 선택하여 예측을 수행하고, 예측 결과를 이미지와 함께 시각화한다. 또한, 테스트 데이터 일부를 이용해 전체 정확도를 평가한다.


5) 성능 평가

학습된 CNN 모델의 정확도는 테스트 데이터에서 약 98% 이상임을 확인한다. Confusion Matrix와 Classification Report를 통해 클래스 별 정밀도와 재현율도 분석한다. 이로써 모델이 손글씨 숫자를 잘 분류함을 확인한다.


(3) 실행 결과 평가

1) 주요 메트릭 (metric)

• Precision

정확도. 특정 클래스(양성 등)로 예측된 값 중 실제로 해당 클래스인 비율. \[ \text{Precision} = \frac{TP}{TP + FP} \]

• Recall

재현율. 실제 해당 클래스 중 모델이 정확히 예측한 비율.

\[ \text{Recall} = \frac{TP}{TP + FN} \]

• F1-Score

Precision과 Recall의 조화 평균.

\[ \text{F1} = 2 \times \frac{\text{Precision} \times \text{Recall}}{\text{Precision} + \text{Recall}} \]

• Support

각 클래스의 실제 데이터 개수.

    Classification Report:
               precision    recall  f1-score   support

           0       0.98      1.00      0.99        42
           1       1.00      1.00      1.00        67
           2       0.98      0.98      0.98        55
           3       0.98      1.00      0.99        45
           4       1.00      1.00      1.00        55
           5       1.00      0.98      0.99        50
           6       1.00      0.98      0.99        43
           7       0.98      1.00      0.99        49
           8       1.00      0.97      0.99        40
           9       1.00      1.00      1.00        54

    accuracy                           0.99       500
    macro avg       0.99      0.99      0.99       500
    weighted avg       0.99      0.99      0.99       500
    

2) confusion_matrix (혼동 행렬)

행은 실제 클래스, 열은 예측 클래스이다.

Actual (실제) Predicted (예측)
Positive (긍정) Negative (부정)
Positive (긍정) TP FN
Negative (부정) FP TN

• True Positive (TP)

실제로 긍정이고 모델도 긍정으로 예측한 경우

• True Negative (TN)

실제로 부정이고 모델도 부정으로 예측한 경우

• False Positive (FP)

실제로 부정인데 모델이 긍정으로 잘못 예측한 경우 (Type I Error)

• False Negative (FN)

실제로 긍정인데 모델이 부정으로 잘못 예측한 경우 (Type II Error)

    Confusion Matrix:
    [[42  0  0  0  0  0  0  0  0  0]
     [ 0 67  0  0  0  0  0  0  0  0]
     [ 0  0 54  0  0  0  0  1  0  0]
     [ 0  0  0 45  0  0  0  0  0  0]
     [ 0  0  0  0 55  0  0  0  0  0]
     [ 0  0  0  1  0 49  0  0  0  0]
     [ 1  0  0  0  0  0 42  0  0  0]
     [ 0  0  0  0  0  0  0 49  0  0]
     [ 0  0  1  0  0  0  0  0 39  0]
     [ 0  0  0  0  0  0  0  0  0 54]]
    

(4) 코딩: DFN(심층 순방향 신경망)


1) 개발 환경 만들기


from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Activation
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.datasets import mnist

import numpy as np                 
import matplotlib.pyplot as plt
    

딥러닝 모델은 다양한 라이브러리를 사용한다.
이를 위해 가장 먼저 사용할 라이브러리를 추가한다.


2) 데이터셋 불러오기


(x_train, y_train), (x_test, y_test) = mnist.load_data()
print("X_train shape", x_train.shape)
print("y_train shape", y_train.shape)
print("X_test shape", x_test.shape)
print("y_test shape", y_test.shape)
    

▶ 실행 결과


x_train shape (60000, 28, 28)
y_train shape (60000,)
x_test shape (10000, 28, 28)
y_test shape (10000,)
    

인공지능 모델을 만들려면 훈련(train) 데이터검증(test) 데이터가 필요하다. 이것은 마치 우리가 학교에서 보는 시험과 같다. 시험 공부할 때는 시험에 무엇이 나올지 알 수 없다. 그래서 여러 내용을 공부한 후 이를 바탕으로 시험을 본다. 인공지능도 마찬가지이다. 인공지능 성능을 살펴보기 위해 학습에 사용한 데이터로 성능을 평가하는 것은 의미가 없다. 학습에 사용하지 않은 데이터를 얼마나 잘 알아 맞히는지가 그 인공지능 성능을 결정한다.


3) MNIST 데이터셋에서 X 형태 바꾸기


X_train = x_train.reshape(60000, 784)
X_test = x_test.reshape(10000, 784)
X_train = X_train.astype('float32')
X_test = X_test.astype('float32')
X_train /= 255                   
X_test /= 255
print("X Training matrix shape", X_train.shape)
print("X Testing matrix shape", X_test.shape)
    

▶ 실행 결과


X Training matrix shape (60000, 784)
X Testing matrix shape (10000, 784)
    

28×28 형태의 데이터를 인공지능 모델에 넣으려면 형태를 바꿔야 한다.
인공신경망의 입력층에 데이터를 넣을 때는 한 줄로 만들어서 넣어야 한다.

...
3×3 형태의 데이터를 1×9 형태의 데이터로 만든 후 딥러닝 모델에 입력한다.
출처: https://wikidocs.net/64066

4) MNIST 데이터셋에서 Y 형태 바꾸기


nb_classes = 10
Y_train = to_categorical(y_train, nb_classes)
Y_test = to_categorical(y_test, nb_classes)
print("Y Training matrix shape", Y_train.shape)
print("Y Testing matrix shape", Y_test.shape)
    

▶ 실행 결과


Y Training matrix shape (60000,10)
Y Testing matrix shape (10000,10)
    

y_train 데이터와 y_test 데이터의 형태를 바꾸어 보자. 인공지능이 분류를 잘할 수 있도록 하기 위해서이다. 우리가 만들고 있는 인공지능은 이미지를 0~9 사이의 숫자로 분류하는 인공지능이다. 이를 다시 살펴보면 인공지능은 이미지가 가진 숫자의 특성,즉 “이 숫자는 3이고 2보다 1이 더 큰 수다.”와 같은 특성은 알 필요 없다. 우리가 만드는 인공지능의 목표는 3과 2를 잘 구분하기만 하면 된다.

따라서 이미지의 레이블(label, 정답)을 인공지능에 0, 1, 2, 3, 4, ···처럼 숫자로 알려 주는 것이 아니라 더 잘 구분할 수 있는 방법으로 알려 줄 필요가 있다. 바로 0은 0이라는 숫자 의미보다 인공지능이 구분할 10개의 숫자 중 첫 번째 숫자로, 1은 1이라는 숫자 의미보다 두 번째 숫자로 말해 주는 것이다.

이를 조금 어려운 말로 표현하면 수치형 데이터범주형 데이터로 변환하는 것이라고 할 수 있다. 이와 같이 몇 번째라는 식으로 알려 주면 인공지능은 더 높은 성능으로 분류할 수 있다. 그래서 예측이 아닌 분류 문제에서는 대부분 정답 레이블을 첫 번째,두 번째,세 번째처럼 순서로 나타내도록 데이터 형태를 바꾼다. 이때 사용하는 방법이 바로 원-핫 인코딩(one-hot incoding)이다.

이와 같이 인공지능을 만들기 위해서는 데이터를 인공지능 모델에서 요구하는 방향으로 분석하여 변환하는 것이 중요하다.


5) 인공지능 모델 설계하기


model = Sequential()
model.add(Dense(512, input_shape=(784,)))
model.add(Activation('relu'))
model.add(Dense(256))
model.add(Activation('relu'))
model.add(Dense(10))
model.add(Activation('softmax'))
model.summary()
    

▶ 실행 결과


Model: "sequential"
------------------------------------
Layer (type) Output Shape Param #
====================================
dense(Dense) (None, 512) 401920
------------------------------------
activation (Activation) (None, 512) 0
------------------------------------
dense_1 (Dense) (None, 256) 131328
------------------------------------
activation_1 (Activation) (None, 256) 0
------------------------------------
dense_2(Dense) (None, 10) 2570
------------------------------------
activation_2 (None, 10) 0
====================================
Total params: 535,818
Trainable params: 535,818
Non-trainable params: 0
    

우리가 설계하고 있는 인공지능 모델은 4개의 층으로 되어 있다. 첫 번째 층은 입력층으로 데이터를 넣는 층이다. 두 번째와 세 번째 층은 은닉층이다. 마지막 네 번째 층은 결과가 출력되는 출력층이다.

...
4개의 층으로 된 인공지능 모델
출처: https://divingintogeneticsandgenomics.com/img/MNIST.png
...
4개의 층으로 된 인공지능 모델
출처: 길벗

우리가 넣는 784개의 데이터가 한 줄로 되어 있기 때문에 입력층의 뉴런 수는 784이다. 앞에서 우리는 28X28 픽셀로 숫자 모습을 784개의 한 줄로 바꾸었다. 이제 이 데이터를 딥러닝 모델에 넣을 예정이며, 첫 번째 은닉층의 노드는 512개로 설정해 보았다. 첫 번째 은닉층에서 두 번째 은닉 층으로 갈 때 활성화(Activation) 함수렐루(ReLU) 함수를 사용할 예정이다.

두 번째 은닉층의 노드는 256개로 설정하였다. 여기서 마지막 층으로 갈 때도 활성화 함수는 렐루 함수를 사용할 예정이다. 마지막 노드가 10개인 이유는 입력된 이미지를 10개로 구분하기 위함이다. 그리고 가장 높은 확률값으로 분류하기 위해서 각 노드의 최종값을 소프트맥스(softmax) 함수를 사용하여 나타내었다.


6) 모델 학습시키기


model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
model.fit(X_train, Y_train, batch_size=128, epochs=10, verbose=1)
    

▶ 실행 결과


Epoch 1/10
  469/469 - 9ms/step - loss: 0.2277 - accuracy: 0.9339
Epoch 2/10
  469/469 - 9ms/step 一 loss: 0.0816 - accuracy: 0.9747
Epoch 3/10
  469/469 - 9ms/step - loss: 0.0522 - accuracy: 0.9840
Epoch 4/10
  469/469 - 9ms/step - loss: 0.0348 - accuracy: 0.9890
Epoch 5/10
  469/469 - 9ms/step - loss: 0.0267 - accuracy: 0.9915
Epoch 6/10
  469/469 - 9ms/step - loss: 0.0222 - accuracy: 0.9926 
Epoch 7/10
  469/469 - 9ms/step - loss: 0.0179 - accuracy: 0.9940
Epoch 8/10
  469/469 - 9ms/step - loss: 0.0173 - accuracy: 0.9941
Epoch 9/10
  469/469 - 9ms/step - loss: 0.0144 - accuracy: 0.9952 
Epoch 10/10
  469/469 - 9ms/step - loss: 0.0112 - accuracy: 0.9963
    

모델을 설계한 후 이 모델을 실행하기 전에 필요한 것이 있다. 심층신경망에 데이터를 흘려보낸 후 정답을 예측할 수 있도록 신경망을 학습하는 과정이 필요하다. 즉, 딥러닝(Deep Learning, DL)을 해야 한다.

데이터를 사용하여 심층 신경망을 딥러닝 기법으로 학습시킨다. 이때 신경망이 예측한 결과와 실제 정답을 비교한 후 오차가 있다면 다시 신경망을 학습시키는 과정을 거친다. 오차가 없다면 더 학습시킬 필요는 없지만 웬만해서는 오차가 0으로 나오는 경우는 거의 없다. 보통 학습시 키는 횟수를 정한 후 그만큼만 학습시킨다.

이처럼 신경망을 잘 학습시키려면 학습한 신경망이 분류한 값과 실젯값의 오차부터 계산해야 한다. 오차를 줄이는 데 경사 하강법(Gradient Descent Method)을 사용한다.

실행 결과를 보면, 첫 번째 에포크(epoch)부터 열 번째 에포크로 갈수록 오차값(loss)이 줄어드는 것을 볼 수 있다. 정확도(accuracy) 또한 지속적으로 증가하는 것을 볼 수 있다.


7) 모델 정확도 살펴보기


score = model.evaluate(X_test, Y_test)
print('Test score:', score[0])
print('Test accuracy:', score[1])
    

▶ 실행 결과


313/313 2ms/step - loss: 0.0817 - accuracy: 0.9800
Test score: 0.08166316896677017
Test accuracy: 0.9800000190734863
    

지금까지 심층 신경망 모델을 설계하고,그 모델을 학습시켰다. 이제 인공지능 모델 성능이 어느 정도인지 시험해 보자. 시험 내용은 ‘검증 데이터를 얼마나 잘 맞히는가?’이다.


8) 모델 학습 결과 확인하기


predicted_classes = np.argmax(model.predict(X_test), axis=1)
correct_indices = np.nonzero(predicted_classes == y_test)[0]
incorrect_indices = np.nonzero(predicted_classes != y_test)[0]
    

▶ 실행 결과


x_train shape (60000, 28, 28)
y_train shape (60000,)
x_test shape (10000, 28, 28)
y_test shape (10000,)
    

지금까지 심층 신경망 모델의 구조를 만들고 그 모델을 학습시킨 후 학습 결과까지 살펴보았다. 이제 실제로 인공지능이 어떤 그림을 무엇으로 예측했는지 잘 구분한 그림과 잘 구분하지 못한 그림을 살펴보자.


9) 잘 예측한 데이터 살펴보기


plt.figure()
for i, correct in enumerate(correct_indices[:9]):
    plt.subplot(3,3,i+1)
    plt.imshow(X_test[correct].reshape(28,28), cmap='gray', interpolation='none')
    plt.title("Predicted {}, Class {}".format(predicted_classes[correct], y_test[correct]))

plt.tight_layout()
    

이제 정확하게 예측한 데이터 위치와 그렇지 않은 데이터 위치를 알게 되었다. 그렇다면 그 데이터는 어떻게 생겼는지 확인해 보자. 실제로 우리가 그 결과를 눈으로 살펴볼 수 있도록 matplotlib 라이브러리를 사용해서 화면에 그래프를 출력해 보자.
실행 결과를 보면 총 9개의 이미지가 나타나며,예측값과 실젯값이 보인다. 가장 첫 번째 그림을 보면 예측값은 7이고 실젯값은 7로 정확하게 예측한 것을 볼 수 있다.

...
예측값과 실젯값이 일치하는지 판별하는 nonzero 함수
출처: https://07lee.tistory.com/87

각 코드 의미를 더 자세히 보자.


10) 잘 예측하지 못한 데이터 살펴보기


plt.figure()
for i, incorrect in enumerate(incorrect_indices[:9]):
    plt.subplot(3,3,i+1)
    plt.imshow(X_test[incorrect].reshape(28,28), cmap='gray', interpolation='none')
    plt.title("Predicted {}, Class {}".format(predicted_classes[incorrect], y_test[incorrect]))

plt.tight_layout()
    

이제 어떤 숫자를 잘 예측하지 못했는지 살펴보자. 코드는 앞의 코드,즉 잘 예측한 데이터 살펴보기 코드와 동일한다. 하지만 변수만 잘 예측하지 못한 그림으로 바뀔 뿐이다.

...
예측값과 실젯값이 일치하는지 판별하는 nonzero 함수
출처: 길벗
앞의 실행 결과는 실제 에포크를 10으로 설정한 후 코드를 실행한 것이다. 마지막 값은 사람이 봐도 0인지 6인지 헷갈린다. 인공지능도 피해 가지는 못했다. 이와 같이 잘못 예측한 값을 확인해 보자.
맞다. 생각보다 인공지능 성능이 높지 않다. 왜 그럴까? 바로 인공지능 모델 학습이 잘되지 않았기 때문이다. 인공지능 모델 학습이 잘되려면 모델의 학습 횟수를 늘려야 한다.

지금까지 첫 번째 인공지능인 숫자를 구분하는 인공지능을 만들어 보았다. 갑자기 어려운 코드 들이 나와서 많이 당황스러웠을 수 있다. 코드 하나하나를 세부 의미까지 이해하는 것도 중요하지만 딥러닝의 개발 흐름을 알아보는 것이 이 책 목표이기에 전반적인 흐름을 먼저 이해하길 추천한다. 우리가 설계한 모델 이외에 수많은 새로운 모델을 다양하게 만들 수 있다. 레이어 수나 각 레이어의 노드 수, 활성화 함수, 에포크 수 등 다양한 파라미터를 수정하여 인공지능을 설계할 수 있다.

숫자를 구분하는 인공지능은 우리가 만든 방법 이외에도 CNN(합성곱 신경망)이라는 방법을 사용하여 만들 수 있다. CNN은 이미지를 인식하는 데 높은 성능을 보이고 있기 때문에 영상 인식 분야에서 주로 사용된다. 우리가 만든 이 신경망이 여러 신경망 알고리즘의 기초가 된다. 이 신경망을 기초로 하여 순환 신경망, 생성적 적대 신경망 등 다양한 딥러닝 알고리즘이 만들어지게 되었다. 그만큼 기초가 되는 신경망이라고 할 수 있다. 따라서 지금까지 잘 따라왔다면 앞으로 살펴볼 딥러닝의 심화된 모델을 학습할 준비가 된 것이다.